﻿//
// (C) Copyright 2009 Irantha Suwandarathna (iranthas@hotmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EffiProz.Core.Results;
using EffiProz.Core.Lib;

namespace EffiProz.Core
{
    public sealed class StatementManager
    {
        /**
         * The Database for which this object is managing
         * CompiledStatement objects.
         */
        private Database database;
        /** Map: Schema id (int) => {Map: SQL String => Compiled Statement id (long)} */
        private IntKeyHashMap schemaMap;

        /** Map: Compiled Statement id (int) => SQL String */
        private LongKeyHashMap sqlLookup;

        /** Map: Compiled statment id (int) => CompiledStatement object. */
        private LongKeyHashMap csidMap;

        /**
         * Monotonically increasing counter used to assign unique ids to compiled
         * statements.
         */
        private long next_cs_id;

        /**
         * Constructs a new instance of <code>CompiledStatementManager</code>.
         *
         * @param database the Database instance for which this object is to
         *      manage compiled statement objects.
         */
        public StatementManager(Database database)
        {

            this.database = database;
            schemaMap = new IntKeyHashMap();
            sqlLookup = new LongKeyHashMap();
            csidMap = new LongKeyHashMap();
            next_cs_id = 0;
        }

        /**
         * Clears all public data structures, removing any references to compiled statements.
         */
        public void reset()
        {
            lock (this)
            {
                schemaMap.clear();
                sqlLookup.clear();
                csidMap.clear();

                next_cs_id = 0;
            }
        }

        /**
         * Retrieves the next compiled statement identifier in the sequence.
         *
         * @return the next compiled statement identifier in the sequence.
         */
        private long nextID()
        {

            next_cs_id++;

            return next_cs_id;
        }

        /**
         * Retrieves the registered compiled statement identifier associated with
         * the specified SQL String, or a value less than zero, if no such
         * statement has been registered.
         *
         * @param schema the schema id
         * @param sql the SQL String
         * @return the compiled statement identifier associated with the
         *      specified SQL String
         */
        private long getStatementID(QNameManager.QName schema, String sql)
        {

             LongValueHashMap sqlMap =
            (LongValueHashMap)schemaMap.get(schema.GetHashCode());

            if (sqlMap == null)
            {
                return -1;
            }

            return sqlMap.get(sql, -1);

        }

        /**
         * Returns an existing CompiledStatement object with the given
         * statement identifier. Returns null if the CompiledStatement object
         * has been invalidated and cannot be recompiled
         *
         * @param session the session
         * @param csid the identifier of the requested CompiledStatement object
         * @return the requested CompiledStatement object
         */
        public Statement getStatement(Session session, long csid)
        {
            lock (this)
            {
                Statement cs = (Statement)csidMap.get(csid);

                if (cs == null)
                {
                    return null;
                }

                if (cs.getCompileTimestamp()
                        < database.schemaManager.getSchemaChangeTimestamp())
                {
                    String sql = (String)sqlLookup.get(csid);
                    QNameManager.QName oldSchema = session.getCurrentSchemaHsqlName();

                    // revalidate with the original schema
                    try
                    {
                        QNameManager.QName schema = cs.getSchemalName();

                        session.setSchema(schema.name);

                        cs = session.compileStatement(sql);

                        session.setSchema(oldSchema.name);
                        cs.setID(csid);
                        cs.setCompileTimestamp(
                            database.txManager.getGlobalChangeTimestamp());
                        csidMap.put(csid, cs);
                    }
                    catch (Exception )
                    {
                        freeStatement(csid);
                        session.setSchema(oldSchema.name);

                        return null;
                    }
                }

                return cs;
            }
        }

        /**
         * Registers a compiled statement to be managed.
         *
         * The only caller should be a Session that is attempting to prepare
         * a statement for the first time or process a statement that has been
         * invalidated due to DDL changes.
         *
         * @param csid existing id or negative if the statement is not yet managed
         * @param cs The CompiledStatement to add
         * @return The compiled statement id assigned to the CompiledStatement
         *  object
         */
        private long registerStatement(long csid, Statement cs)
        {

            if (csid < 0)
            {
                csid = nextID();

                int schemaid = cs.getSchemalName().GetHashCode();
                LongValueHashMap sqlMap =
                    (LongValueHashMap)schemaMap.get(schemaid);

                if (sqlMap == null)
                {
                    sqlMap = new LongValueHashMap();

                    schemaMap.put(schemaid, sqlMap);
                }

                sqlMap.put(cs.getSQL(), csid);
                sqlLookup.put(csid, cs.getSQL());
            }

            cs.setID(csid);
            cs.setCompileTimestamp(database.txManager.getGlobalChangeTimestamp());
            csidMap.put(csid, cs);

            return csid;
        }

        /**
         * Removes one (or all) of the links between a session and a compiled
         * statement. If the statement is not linked with any other session, it is
         * removed from management.
         *
         * @param csid the compiled statment identifier
         * @param sessionID the session identifier
         * @param freeAll if true, remove all links to the session
         */
        public void freeStatement(long csid)
        {
            lock (this)
            {
                if (csid == -1)
                {

                    // statement was never added
                    return;
                }

                Statement cs = (Statement)csidMap.remove(csid);

                if (cs != null)
                {
                    int schemaid = cs.getSchemalName().GetHashCode();
                    LongValueHashMap sqlMap =
                        (LongValueHashMap)schemaMap.get(schemaid);
                    String sql = (String)sqlLookup.remove(csid);

                    sqlMap.remove(sql);
                }
            }
        }

        /**
         * Compiles an SQL statement and returns a CompiledStatement Object
         *
         * @param session the session
         * @throws Throwable
         * @return CompiledStatement
         */
        public Statement compile(Session session,
                                       Result cmd)
        {
            lock (this)
            {
                String sql = cmd.getMainString();
                long csid = getStatementID(session.currentSchema, sql);
                Statement cs = (Statement)csidMap.get(csid);

                if (cs == null || !cs.isValid())
                {
                    cs = session.compileStatement(sql);
                    csid = registerStatement(csid, cs);
                }

                return cs;
            }
        }
    }
}
